/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.test.context.web;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.Conventions;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.util.Assert;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletWebRequest;

import javax.servlet.ServletContext;

/**
 * {@code TestExecutionListener} which provides mock Servlet API support to
 * {@link WebApplicationContext WebApplicationContexts} loaded by the <em>Spring
 * TestContext Framework</em>.
 *
 * <p>Specifically, {@code ServletTestExecutionListener} sets up thread-local
 * state via Spring Web's {@link RequestContextHolder} during {@linkplain
 * #prepareTestInstance(TestContext) test instance preparation} and {@linkplain
 * #beforeTestMethod(TestContext) before each test method} and creates a {@link
 * MockHttpServletRequest}, {@link MockHttpServletResponse}, and
 * {@link ServletWebRequest} based on the {@link MockServletContext} present in
 * the {@code WebApplicationContext}. This listener also ensures that the
 * {@code MockHttpServletResponse} and {@code ServletWebRequest} can be injected
 * into the test instance, and once the test is complete this listener {@linkplain
 * #afterTestMethod(TestContext) cleans up} thread-local state.
 *
 * <p>Note that {@code ServletTestExecutionListener} is enabled by default but
 * generally takes no action if the {@linkplain TestContext#getTestClass() test
 * class} is not annotated with {@link WebAppConfiguration @WebAppConfiguration}.
 * See the javadocs for individual methods in this class for details.
 *
 * @author Sam Brannen
 * @author Phillip Webb
 * @since 3.2
 */
public class ServletTestExecutionListener extends AbstractTestExecutionListener {

    /**
     * Attribute name for a {@link TestContext} attribute which indicates
     * whether or not the {@code ServletTestExecutionListener} should {@linkplain
     * RequestContextHolder#resetRequestAttributes() reset} Spring Web's
     * {@code RequestContextHolder} in {@link #afterTestMethod(TestContext)}.
     * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
     */
    public static final String RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE = Conventions.getQualifiedAttributeName(
            ServletTestExecutionListener.class, "resetRequestContextHolder");

    /**
     * Attribute name for a {@link TestContext} attribute which indicates that
     * {@code ServletTestExecutionListener} has already populated Spring Web's
     * {@code RequestContextHolder}.
     * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
     */
    public static final String POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE = Conventions.getQualifiedAttributeName(
            ServletTestExecutionListener.class, "populatedRequestContextHolder");

    /**
     * Attribute name for a request attribute which indicates that the
     * {@link MockHttpServletRequest} stored in the {@link RequestAttributes}
     * in Spring Web's {@link RequestContextHolder} was created by the TestContext
     * framework.
     * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
     *
     * @since 4.2
     */
    public static final String CREATED_BY_THE_TESTCONTEXT_FRAMEWORK = Conventions.getQualifiedAttributeName(
            ServletTestExecutionListener.class, "createdByTheTestContextFramework");

    /**
     * Attribute name for a {@link TestContext} attribute which indicates that the
     * {@code ServletTestExecutionListener} should be activated. When not set to
     * {@code true}, activation occurs when the {@linkplain TestContext#getTestClass()
     * test class} is annotated with {@link WebAppConfiguration @WebAppConfiguration}.
     * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}.
     *
     * @since 4.3
     */
    public static final String ACTIVATE_LISTENER = Conventions.getQualifiedAttributeName(
            ServletTestExecutionListener.class, "activateListener");


    private static final Log logger = LogFactory.getLog(ServletTestExecutionListener.class);


    /**
     * Returns {@code 1000}.
     */
    @Override
    public final int getOrder() {
        return 1000;
    }

    /**
     * Sets up thread-local state during the <em>test instance preparation</em>
     * callback phase via Spring Web's {@link RequestContextHolder}, but only if
     * the {@linkplain TestContext#getTestClass() test class} is annotated with
     * {@link WebAppConfiguration @WebAppConfiguration}.
     *
     * @see TestExecutionListener#prepareTestInstance(TestContext)
     * @see #setUpRequestContextIfNecessary(TestContext)
     */
    @Override
    public void prepareTestInstance(TestContext testContext) throws Exception {
        setUpRequestContextIfNecessary(testContext);
    }

    /**
     * Sets up thread-local state before each test method via Spring Web's
     * {@link RequestContextHolder}, but only if the
     * {@linkplain TestContext#getTestClass() test class} is annotated with
     * {@link WebAppConfiguration @WebAppConfiguration}.
     *
     * @see TestExecutionListener#beforeTestMethod(TestContext)
     * @see #setUpRequestContextIfNecessary(TestContext)
     */
    @Override
    public void beforeTestMethod(TestContext testContext) throws Exception {
        setUpRequestContextIfNecessary(testContext);
    }

    /**
     * If the {@link #RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} in the supplied
     * {@code TestContext} has a value of {@link Boolean#TRUE}, this method will
     * (1) clean up thread-local state after each test method by {@linkplain
     * RequestContextHolder#resetRequestAttributes() resetting} Spring Web's
     * {@code RequestContextHolder} and (2) ensure that new mocks are injected
     * into the test instance for subsequent tests by setting the
     * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE}
     * in the test context to {@code true}.
     * <p>The {@link #RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} and
     * {@link #POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} will be subsequently
     * removed from the test context, regardless of their values.
     *
     * @see TestExecutionListener#afterTestMethod(TestContext)
     */
    @Override
    public void afterTestMethod(TestContext testContext) throws Exception {
        if (Boolean.TRUE.equals(testContext.getAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE))) {
            if (logger.isDebugEnabled()) {
                logger.debug(String.format("Resetting RequestContextHolder for test context %s.", testContext));
            }
            RequestContextHolder.resetRequestAttributes();
            testContext.setAttribute(DependencyInjectionTestExecutionListener.REINJECT_DEPENDENCIES_ATTRIBUTE,
                    Boolean.TRUE);
        }
        testContext.removeAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE);
        testContext.removeAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE);
    }

    private boolean isActivated(TestContext testContext) {
        return (Boolean.TRUE.equals(testContext.getAttribute(ACTIVATE_LISTENER)) ||
                AnnotatedElementUtils.hasAnnotation(testContext.getTestClass(), WebAppConfiguration.class));
    }

    private boolean alreadyPopulatedRequestContextHolder(TestContext testContext) {
        return Boolean.TRUE.equals(testContext.getAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE));
    }

    private void setUpRequestContextIfNecessary(TestContext testContext) {
        if (!isActivated(testContext) || alreadyPopulatedRequestContextHolder(testContext)) {
            return;
        }

        ApplicationContext context = testContext.getApplicationContext();

        if (context instanceof WebApplicationContext) {
            WebApplicationContext wac = (WebApplicationContext) context;
            ServletContext servletContext = wac.getServletContext();
            Assert.state(servletContext instanceof MockServletContext, () -> String.format(
                    "The WebApplicationContext for test context %s must be configured with a MockServletContext.",
                    testContext));

            if (logger.isDebugEnabled()) {
                logger.debug(String.format(
                        "Setting up MockHttpServletRequest, MockHttpServletResponse, ServletWebRequest, and RequestContextHolder for test context %s.",
                        testContext));
            }

            MockServletContext mockServletContext = (MockServletContext) servletContext;
            MockHttpServletRequest request = new MockHttpServletRequest(mockServletContext);
            request.setAttribute(CREATED_BY_THE_TESTCONTEXT_FRAMEWORK, Boolean.TRUE);
            MockHttpServletResponse response = new MockHttpServletResponse();
            ServletWebRequest servletWebRequest = new ServletWebRequest(request, response);

            RequestContextHolder.setRequestAttributes(servletWebRequest);
            testContext.setAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE);
            testContext.setAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE);

            if (wac instanceof ConfigurableApplicationContext) {
                @SuppressWarnings("resource")
                ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) wac;
                ConfigurableListableBeanFactory bf = configurableApplicationContext.getBeanFactory();
                bf.registerResolvableDependency(MockHttpServletResponse.class, response);
                bf.registerResolvableDependency(ServletWebRequest.class, servletWebRequest);
            }
        }
    }

}
